package pab.ui.auxiliary;

import java.awt.Component;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;

import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.UIManager;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

public final class AquaLnFPopupLocationFix {

    /** the components being fixed */
    private final JComboBox comboBox;
    private final JPopupMenu popupMenu;

    /** the listener provides callbacks as necessary */
    private final Listener listener = new Listener();

    /**
     * Private constructor so users use the more action-oriented
     * {@link #install} method.
     */
    private AquaLnFPopupLocationFix(JComboBox comboBox) {
        this .comboBox = comboBox;
        this .popupMenu = (JPopupMenu) comboBox.getUI()
                .getAccessibleChild(comboBox, 0);

        popupMenu.addPopupMenuListener(listener);
    }

    /**
     * Install the fix for the specified combo box.
     */
    public static AquaLnFPopupLocationFix install(JComboBox comboBox) {
        if (comboBox == null)
            throw new IllegalArgumentException();
        return new AquaLnFPopupLocationFix(comboBox);
    }

    /**
     * Uninstall the fix. Usually this is unnecessary since letting the combo
     * box go out of scope is sufficient.
     */
    public void uninstall() {
        popupMenu.removePopupMenuListener(listener);
    }

    /**
     * Reposition the popup immediately before it is shown.
     */
    private class Listener implements  PopupMenuListener {
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            final JComponent popupComponent = (JComponent) e
                    .getSource();
            fixPopupLocation(popupComponent);
        }

        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            // do nothing
        }

        public void popupMenuCanceled(PopupMenuEvent e) {
            // do nothing
        }
    }

    /**
     * Do the adjustment on the specified popupComponent immediately before
     * it is displayed.
     */
    private void fixPopupLocation(JComponent popupComponent) {
        // we only need to fix Apple's aqua look and feel
        if (popupComponent.getClass().getName().indexOf("apple.laf") != 0) {
            return;
        }

        // put the popup right under the combo box so it looks like a
        // normal Aqua combo box
        Point comboLocationOnScreen = comboBox.getLocationOnScreen();
        int comboHeight = comboBox.getHeight();
        int popupY = comboLocationOnScreen.y + comboHeight;

        // ...unless the popup overflows the screen, in which case we put it
        // above the combobox
        Rectangle screenBounds = new ScreenGeometry(comboBox)
                .getScreenBounds();
        int popupHeight = popupComponent.getPreferredSize().height;
        if (comboLocationOnScreen.y + comboHeight + popupHeight > screenBounds.x
                + screenBounds.height) {
            popupY = comboLocationOnScreen.y - popupHeight;
        }

        popupComponent.setLocation(comboLocationOnScreen.x, popupY);
    }
    
    private final static class ScreenGeometry {

        final GraphicsConfiguration graphicsConfiguration;
        final boolean aqua;

        public ScreenGeometry(JComponent component) {
            this .aqua = UIManager.getLookAndFeel().getName().indexOf(
                    "Aqua") != -1;
            this .graphicsConfiguration = graphicsConfigurationForComponent(component);
        }

        /**
         * Get the best graphics configuration for the specified point and component.
         */
        private GraphicsConfiguration graphicsConfigurationForComponent(
                Component component) {
            Point point = component.getLocationOnScreen();

            // try to find the graphics configuration for our point of interest
            GraphicsEnvironment ge = GraphicsEnvironment
                    .getLocalGraphicsEnvironment();
            GraphicsDevice[] gd = ge.getScreenDevices();
            for (int i = 0; i < gd.length; i++) {
                if (gd[i].getType() != GraphicsDevice.TYPE_RASTER_SCREEN)
                    continue;
                GraphicsConfiguration defaultGraphicsConfiguration = gd[i]
                        .getDefaultConfiguration();
                if (!defaultGraphicsConfiguration.getBounds().contains(
                        point))
                    continue;
                return defaultGraphicsConfiguration;
            }

            // we couldn't find a graphics configuration, use the component's
            return component.getGraphicsConfiguration();
        }

        /**
         * Get the bounds of where we can put a popup.
         */
        public Rectangle getScreenBounds() {
            Rectangle screenSize = getScreenSize();
            Insets screenInsets = getScreenInsets();

            return new Rectangle(screenSize.x + screenInsets.left,
                    screenSize.y + screenInsets.top, screenSize.width
                            - screenInsets.left - screenInsets.right,
                    screenSize.height - screenInsets.top
                            - screenInsets.bottom);
        }

        /**
         * Get the bounds of the screen currently displaying the component.
         */
        public Rectangle getScreenSize() {
            // get the screen bounds and insets via the graphics configuration
            if (graphicsConfiguration != null) {
                return graphicsConfiguration.getBounds();
            }

            // just use the toolkit bounds, it's less awesome but sufficient
            return new Rectangle(Toolkit.getDefaultToolkit()
                    .getScreenSize());
        }

        /**
         * Fetch the screen insets, the off limits areas around the screen such
         * as menu bar, dock or start bar.
         */
        public Insets getScreenInsets() {
            Insets screenInsets;
            if (graphicsConfiguration != null) {
                screenInsets = Toolkit.getDefaultToolkit()
                        .getScreenInsets(graphicsConfiguration);
            } else {
                screenInsets = new Insets(0, 0, 0, 0);
            }

            // tweak the insets for aqua, they're reported incorrectly there
            if (aqua) {
                int aquaBottomInsets = 21; // unreported insets, shown in screenshot, https://glazedlists.dev.java.net/issues/show_bug.cgi?id=332
                int aquaTopInsets = 22; // for Apple menu bar, found via debugger

                screenInsets.bottom = Math.max(screenInsets.bottom,
                        aquaBottomInsets);
                screenInsets.top = Math.max(screenInsets.top,
                        aquaTopInsets);
            }

            return screenInsets;
        }
    }
}

